﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media.Imaging;
using System.Xml;

namespace FB2er
{
    public class FB2XmlTransformer
    {
        private readonly XmlReader _reader;

        public FB2XmlTransformer(XmlReader reader)
        {
            _reader = reader;
        }

        public IObservable<BlockHolder> Blocks
        {
            get
            {
                return RunTemplate
                    .ObserveOnDispatcher()
                    .Scan((BlockHolder)new ParagraphHolder(), (prg, rt) => prg.ApplyRunTemplate(rt))
                    .DistinctUntilChanged();
            }
        }

        public IObservable<RunTemplate> RunTemplate
        {
            get
            {
                return FB2Iterator()
                    .ToObservable()
                    .Select(r => new XmlNodeData(r))
                    .Publish(CreateTempate);
            }
        }

        private static IObservable<RunTemplate> CreateTempate(IObservable<XmlNodeData> input)
        {
            var rts = input
                .Scan(new RunTemplate(), (rt, nd) => rt.ApplyXMLNode(nd))
                .Where(rt => !string.IsNullOrEmpty(rt.Text))
                .DistinctUntilChanged()
                .BufferAndProcess(ProcessTemplates);
            return rts;
        }

        private static IEnumerable<RunTemplate> ProcessTemplates(IEnumerable<RunTemplate> templates)
        {
            templates
                .Where(rt => rt.IsImage)
                .SelectMany(img => templates.Where(bnr => bnr.IsBinaryFor(img.HRef)), (img, bnr) => new { img, bnr })
                .Do(imgPair => imgPair.img.SetImageSource(imgPair.bnr))
                .ToArray();
            templates
                .Where(rt => rt.IsAnchor)
                .SelectMany(anc => templates.Where(note => note.IsNoteFor(anc.HRef)), (anc, note) => new { anc, note })
                .Do(notePair => notePair.anc.SetNotes(notePair.note))
                .ToArray();
            return templates.Where(rt =>
                !rt.Elements.Contains("binary") &&
                (!rt.Elements.Contains("description") || rt.Elements.Contains("image"))
                );
        }

        private IEnumerable<XmlReader> FB2Iterator()
        {
            Debug.WriteLine("enumerating xml");
            while (_reader.Read())
            {
                yield return _reader;
            }
        }
    }

    public class XmlNodeData
    {
        public XmlNodeData()
        { }

        public XmlNodeData(XmlReader xmlReader)
        {
            NodeType = xmlReader.NodeType;
            NodeName = xmlReader.Name;
            if (NodeName == "body" && xmlReader["name"] == "notes")
                NodeName = "notes";  // "notes" element is more useful than just "body"
            IsEmpty = xmlReader.IsEmptyElement;
            HRef = xmlReader["l:href"];
            if (string.IsNullOrEmpty(HRef))
                HRef = xmlReader["xlink:href"];
            if (!string.IsNullOrEmpty(HRef) && HRef[0] == '#')
                HRef = HRef.Substring(1);
            ID = xmlReader["id"];
            ContentType = xmlReader["content-type"];
            if (NodeType == XmlNodeType.Text)
                Text = xmlReader.Value;
        }

        public XmlNodeType NodeType { get; set; }
        public string NodeName { get; set; }
        public string Text { get; set; }
        public bool IsEmpty { get; set; }
        public string HRef { get; set; }
        public string ID { get; set; }
        public string ContentType { get; set; }
    }

    public class RunTemplate
    {
        public List<string> Elements = new List<string>();
        public List<Tuple<string, string, string>> Attributes = new List<Tuple<string, string, string>>();
        public BitmapSource ImageSource { get; private set; }
        public Paragraph NotesParagraph { get; private set; }
        public string Text { get; set; }
        public int NEmptyLines { get; set; }
        public bool KeepWithNext { get; set; }
        public bool KeepWithPrevious { get; set; }

        public RunTemplate()
        {
        }

        public RunTemplate(RunTemplate fromTemplate)
        {
            if (fromTemplate.Elements.Contains("a"))
                KeepWithPrevious = true;
            Elements.AddRange(fromTemplate.Elements.Where(m => !m.Equals("image")));
            Attributes.AddRange(fromTemplate.Attributes.Where(a => !a.Item1.Equals("image")));
        }

        public RunTemplate ApplyXMLNode(XmlNodeData nd)
        {
            switch (nd.NodeType)
            {
                case XmlNodeType.Element:
                    if (nd.IsEmpty)
                    {
                        if (nd.NodeName == "empty-line")
                            NEmptyLines++;
                        if (nd.NodeName == "image")
                            return new RunTemplate(this).SetImage(nd);
                        return this;
                    }
                    if (nd.NodeName == "a")
                        KeepWithNext = true;
                    return string.IsNullOrEmpty(Text) ?
                        AddElement(nd) :
                        new RunTemplate(this).AddElement(nd);
                case XmlNodeType.EndElement:
                    return string.IsNullOrEmpty(Text) ?
                        RemoveElement(nd.NodeName) :
                        new RunTemplate(this).RemoveElement(nd.NodeName);
                case XmlNodeType.Text:
                    Text = nd.Text;
                    return this;
                default:
                    return this;
            }
        }

        private RunTemplate SetImage(XmlNodeData nd)
        {
            Elements.Add(nd.NodeName);
            Attributes.Add(Tuple.Create(nd.NodeName, "href", nd.HRef));
            Text = nd.HRef;  //placeholder
            return this;
        }

        private RunTemplate AddElement(XmlNodeData nd)
        {
            if (nd.NodeName != "FictionBook") // we do not need root element at each template part
                Elements.Add(nd.NodeName);
            if (!string.IsNullOrEmpty(nd.HRef))
                Attributes.Add(Tuple.Create(nd.NodeName, "href", nd.HRef));
            if (!string.IsNullOrEmpty(nd.ID))
                Attributes.Add(Tuple.Create(nd.NodeName, "id", nd.ID));
            if (!string.IsNullOrEmpty(nd.ContentType))
                Attributes.Add(Tuple.Create(nd.NodeName, "ctype", nd.ContentType));
            return this;
        }

        private RunTemplate RemoveElement(string element)
        {
            Elements.Remove(element);
            Attributes = Attributes.Where(attr => attr.Item1 != element).ToList();
            return this;
        }

        public bool IsImage
        {
            get { return Elements.Contains("image"); }
        }

        public bool IsAnchor
        {
            get { return Elements.Contains("a"); }
        }

        public bool IsNote
        {
            get { return Elements.Contains("notes") && NotesParagraph != null; }
        }

        public string Attr(string attrID)
        {
            var attr = Attributes.Where(a => a.Item2 == attrID).FirstOrDefault();
            return attr == null ? "" : attr.Item3;
        }

        public string HRef { get { return Attr("href"); } }


        public bool IsBinaryFor(string hRef)
        {
            return Elements.Contains("binary") && Attr("id") == hRef;
        }

        public bool IsNoteFor(string hRef)
        {
            return Elements.Contains("notes") && Attr("id") == hRef;
        }

        public void SetImageSource(RunTemplate binary)
        {
            ImageSource = ProcessBinary(binary.Text, binary.Attr("ctype"));
        }

        public void SetNotes(RunTemplate note)
        {
            if (NotesParagraph == null)
                NotesParagraph = new Paragraph();
            note.NotesParagraph = NotesParagraph;
        }

        private static BitmapSource ProcessBinary(string encodedImage, string contentType)
        {
            var ms = new MemoryStream(Convert.FromBase64String(encodedImage));
            BitmapFrame imgSrc = null;
            switch (contentType)
            {
                case "image/jpeg":
                    imgSrc = new JpegBitmapDecoder(ms, BitmapCreateOptions.None, BitmapCacheOption.None).Frames[0];
                    break;
                case "image/jpg":
                    imgSrc = new JpegBitmapDecoder(ms, BitmapCreateOptions.None, BitmapCacheOption.None).Frames[0];
                    break;
                case "image/png":
                    imgSrc = new PngBitmapDecoder(ms, BitmapCreateOptions.None, BitmapCacheOption.None).Frames[0];
                    break;
                default:
                    Debug.WriteLine("Unknown image type : {0}", contentType);
                    break;
            }
            return imgSrc;
        }
    }

    public enum ParagraphType
    {
        Body,
        Title,
        Epigraph,
        Image
    }

    public abstract class BlockHolder
    {
        public abstract BlockHolder ApplyRunTemplate(RunTemplate rt);
        public abstract Block Block();
        public static ParagraphType GetParagraphType(RunTemplate rt)
        {
            if (rt.Elements.Contains("title"))
                return ParagraphType.Title;
            if (rt.Elements.Contains("epigraph"))
                return ParagraphType.Epigraph;
            if (rt.IsImage)
                return ParagraphType.Image;
            return ParagraphType.Body;
        }
    }

    public class ImageHolder : BlockHolder
    {
        private readonly BlockUIContainer _container = new BlockUIContainer();

        public override BlockHolder ApplyRunTemplate(RunTemplate rt)
        {
            if (_container.Child == null)
            {
                _container.Child =
                    new Image
                        {
                            Source = rt.ImageSource,
                            MaxHeight = rt.ImageSource.PixelHeight,
                            MaxWidth = rt.ImageSource.PixelWidth,
                        };
                return this;
            }
            return GetParagraphType(rt) == ParagraphType.Image ?
                new ImageHolder().ApplyRunTemplate(rt) :
                new ParagraphHolder().ApplyRunTemplate(rt);
        }

        public override Block Block()
        {
            return _container;
        }
    }

    public class ParagraphHolder : BlockHolder
    {
        private readonly Paragraph _paragraph = new Paragraph();

        public override BlockHolder ApplyRunTemplate(RunTemplate rt)
        {
#if DEBUG
            LogUnknownTypes(rt);
#endif
            var neededType = GetParagraphType(rt);

            if (neededType == ParagraphType.Image)
                return new ImageHolder().ApplyRunTemplate(rt);

            if (_paragraph.Inlines.Count > 0 && neededType != _paragraphType)
            {
                return new ParagraphHolder().ApplyRunTemplate(rt);
            }

            _paragraphType = neededType;

            if (rt.IsAnchor)
                ApplyAnchorFormatting(rt);
            else
                if (rt.IsNote)
                    ApplyNotesFormatting(rt);
                else
                    ApplyRunFormatting(rt);
            return this;
        }

        private void ApplyRunFormatting(RunTemplate rt)
        {
            var run = new Run(rt.KeepWithPrevious ? rt.Text : '\u2003' + rt.Text);
            if (rt.Elements.Contains("title"))
            {
                TextElement.SetFontSize(run, 20);
                TextElement.SetFontWeight(run, FontWeights.Bold);
            }
            if (rt.Elements.Contains("subtitle"))
            {
                TextElement.SetFontSize(run, 16);
                TextElement.SetFontWeight(run, FontWeights.Bold);
            }
            if (rt.Elements.Contains("emphasis"))
                TextElement.SetFontStyle(run, FontStyles.Italic);
            if (rt.Elements.Contains("strong"))
                TextElement.SetFontWeight(run, FontWeights.Bold);
            if (rt.Elements.Contains("epigraph"))
            {
                TextElement.SetFontStyle(run, FontStyles.Italic);
                _paragraph.TextAlignment = TextAlignment.Right;
            }
            if (rt.Elements.Contains("text-author"))
            {
                TextElement.SetFontWeight(run, FontWeights.Bold);
                TextElement.SetFontStyle(run, FontStyles.Italic);
            }
            for (var i = 0; i < rt.NEmptyLines; i++)
            {
                _paragraph.Inlines.Add(new LineBreak());
            }
            _paragraph.Inlines.Add(run);
            if (!rt.KeepWithNext)
                _paragraph.Inlines.Add(new LineBreak());
            if (rt.Elements.Contains("text-author"))
                _paragraph.Inlines.Add(new LineBreak());
        }

        private static void ApplyNotesFormatting(RunTemplate rt)
        {
            Run run;
            if (rt.Elements.Contains("title"))
            {
                run = new Run(rt.Text)
                          {
                              BaselineAlignment = BaselineAlignment.Superscript
                          };
            }
            else
            {
                run = new Run('\u2003' + rt.Text);
            }
            TextElement.SetFontSize(run, 10);
            rt.NotesParagraph.Inlines.Add(run);
        }

        private void ApplyAnchorFormatting(RunTemplate rt)
        {
            if (rt.HRef.Contains(":"))
            {
                Uri uri;
                try
                {
                    uri = new Uri(rt.HRef);
                }
                catch (Exception)
                {
                    uri = null;
                }
                var hl = new Hyperlink(new Run(rt.Text))
                {
                    NavigateUri = uri,
                };
                hl.Click += (sender, args) =>
                {
                    if (((Hyperlink)sender).NavigateUri != null)
                        Process.Start(((Hyperlink)sender).NavigateUri.AbsoluteUri);
                };
                _paragraph.Inlines.Add(hl);
            }
            else
            {

                var run = new Run(rt.Text)
                 {
                     BaselineAlignment = BaselineAlignment.Superscript
                 };
                _paragraph.Inlines.Add(run);
                var figure = new Figure(rt.NotesParagraph)
                {
                    Width = new FigureLength(1, FigureUnitType.Content),
                    VerticalAnchor = FigureVerticalAnchor.PageBottom,
                    HorizontalAnchor = FigureHorizontalAnchor.ColumnCenter,
                    BorderThickness = new Thickness(0, 1, 0, 0),
                    BorderBrush = SystemColors.ActiveBorderBrush,
                };
                _paragraph.Inlines.Add(figure);
            }
        }

        private ParagraphType _paragraphType;

#if DEBUG
        private static readonly string[] KnownElements = { "p", "section", "body", "title", "subtitle", "image", "description", "title-info", "coverpage", "emphasis", "a", "notes", "strong", "epigraph", "text-author" };

        private static void LogUnknownTypes(RunTemplate rt)
        {
            rt.Elements.Except(KnownElements).Do(Console.WriteLine).ToArray();
        }
#endif
        public override Block Block()
        {
            return _paragraph;
        }
    }

    public static class ObservbleEx
    {
        public static IObservable<T> BufferAndProcess<T>(this IObservable<T> source, Func<IEnumerable<T>, IEnumerable<T>> process)
        {
            return Observable.Defer(() =>
            {
                var buffer = new List<T>();
                return source.SelectMany(
                    x =>
                    {
                        buffer.Add(x);
                        return Observable.Empty<T>();
                    },
                    Observable.Throw<T>,
                    () => process(buffer).ToObservable());
            });
        }
    }
}
